package com.cysyz.mylib.jpadao;


import com.cysyz.mylib.dao.PagingResult;

import javax.persistence.EntityManager;
import javax.persistence.NoResultException;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.ParameterExpression;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class CriteriaQueryJPA<T> implements Serializable {

    public final Condition condition = new Condition();

    private static final long serialVersionUID = 5064932771068929342L;


    private EntityManager entityManager;

    /**
     * 要查询的模型对象
     */
    private Class clazz;

    /**
     * 查询条件列表
     */
    private Root from;

    private List<Predicate> predicates;

    private javax.persistence.criteria.CriteriaQuery criteriaQuery;

    private CriteriaBuilder criteriaBuilder;

    /**
     * 排序方式列表
     */
    private List<Order> orders;

    /**
     * 关联模式
     */
    private Map<String, CriteriaQueryJPA> subQuery;

    private Map<String, CriteriaQueryJPA> linkQuery;

    private String projection;


    private String groupBy;

    CriteriaQueryJPA(Class<T> clazz, EntityManager entityManager) {
        this.clazz = clazz;
        this.entityManager = entityManager;
        this.criteriaBuilder = this.entityManager.getCriteriaBuilder();
        this.criteriaQuery = criteriaBuilder.createQuery(this.clazz);
        this.from = criteriaQuery.from(this.clazz);
        predicates = new ArrayList();
        this.orders = new ArrayList();
    }

    /** 通过类创建查询条件 */
/*
    static CriteriaQuery forClass(Class clazz, EntityManager entityManager) {
        return new CriteriaQuery(clazz, entityManager);
    }
 */

    /**
     * 增加子查询
     */
    private void addSubQuery(String propertyName, CriteriaQueryJPA query) {
        if (this.subQuery == null)
            this.subQuery = new HashMap();

        if (query.projection == null)
            throw new RuntimeException("子查询字段未设置");

        this.subQuery.put(propertyName, query);
    }

    private void addSubQuery(CriteriaQueryJPA query) {
        addSubQuery(query.projection, query);
    }

    /**
     * 增关联查询
     */
    public void addLinkQuery(String propertyName, CriteriaQueryJPA query) {
        if (this.linkQuery == null)
            this.linkQuery = new HashMap();

        this.linkQuery.put(propertyName, query);
    }

    Set<ParameterInfo> parameterInfos = new HashSet<>();

    private class ParameterInfo {
        private ParameterExpression parameterExpression;
        private Object value;

        public ParameterInfo(ParameterExpression parameterExpression, Object value) {
            this.parameterExpression = parameterExpression;
            this.value = value;
        }

        public ParameterExpression getParameterExpression() {
            return parameterExpression;
        }

        public Object getValue() {
            return value;
        }
    }


    public class Condition {

        public final CriteriaQueryJPA<T> query = CriteriaQueryJPA.this;

        private Condition() {
        }

        public Condition or() {
            Predicate predicate = criteriaBuilder.or(predicates.toArray(new Predicate[predicates.size()]));
            predicates.clear();
            predicates.add(predicate);
            return this;
        }

        public Condition and() {
            Predicate predicate = criteriaBuilder.and(predicates.toArray(new Predicate[predicates.size()]));
            predicates.clear();
            predicates.add(predicate);
            return this;
        }

        /**
         * 相等
         */
        public Condition eq(String propertyName, Object value) {


            predicates.add(criteriaBuilder.equal(from.get(propertyName), getParameterExpression(value)));
            return this;
        }

        /**
         * 不相等
         */
        public Condition notEq(String propertyName, Object value) {

            predicates.add(criteriaBuilder.notEqual(from.get(propertyName), getParameterExpression(value)));
            return this;
        }

        /**
         * 模糊匹配
         *
         * @param propertyName 属性名称
         * @param value
         */
        public Condition like(String propertyName, String value) {
            if (value.indexOf("%") < 0) {
                value = "%" + value + "%";
            }

            predicates.add(criteriaBuilder.like(from.get(propertyName), getParameterExpression(value)));
            return this;
        }

        /**
         * 暂时不需要此方法
         *
         * @param propertyName
         * @param value
         * @return
         */
        private Condition orLike(List<String> propertyName, String value) {

            if (value.indexOf("%") < 0) {
                value = "%" + value + "%";
            }

            Predicate predicate = criteriaBuilder.or(criteriaBuilder.like(from.get(propertyName.get(0)), value.toString()));
            for (int i = 1; i < propertyName.size(); ++i) {
                predicate = criteriaBuilder.or(predicate, criteriaBuilder.like(from.get(propertyName.get(i)), value));
            }
            predicates.add(predicate);
            return this;
        }

        /**
         * 空
         */
        public Condition isNull(String propertyName) {
            predicates.add(criteriaBuilder.isNull(from.get(propertyName)));
            return this;
        }

        /**
         * 非空
         */
        public Condition isNotNull(String propertyName) {
            predicates.add(criteriaBuilder.isNotNull(from.get(propertyName)));
            return this;
        }

        /**
         * in
         *
         * @param propertyName 属性名称
         * @param value
         */
        public Condition in(String propertyName, Collection value) {

            Iterator iterator = value.iterator();
            CriteriaBuilder.In in = criteriaBuilder.in(from.get(propertyName));
            while (iterator.hasNext()) {
                in.value(getParameterExpression(iterator.next()));
            }
            predicates.add(in);
            return this;

        }

        /**
         * not in
         *
         * @param propertyName 属性名称
         * @param value
         */
        public Condition notIn(String propertyName, Collection value) {

            Iterator iterator = value.iterator();
            CriteriaBuilder.In in = criteriaBuilder.in(from.get(propertyName));
            while (iterator.hasNext()) {
                in.value(getParameterExpression(iterator.next()));
            }
            predicates.add(criteriaBuilder.not(in));
            return this;
        }

        /**
         * 时间区间查询
         *
         * @param propertyName 属性名称
         * @param lo           属性起始值
         * @param go
         */
        public Condition between(String propertyName, Date lo, Date go) {
            if (!isNullOrEmpty(lo) && !isNullOrEmpty(go)) {
                predicates.add(criteriaBuilder.between(from.get(propertyName), getParameterExpression(lo), getParameterExpression(go)));
            }

            // if (!isNullOrEmpty(lo) && !isNullOrEmpty(go)) {
            // predicates.add(criteriaBuilder.lessThan(from.get(propertyName),
            // new DateTime(lo).toString()));
            // }
            // if (!isNullOrEmpty(go)) {
            // predicates.add(criteriaBuilder.greaterThan(from.get(propertyName),
            // new DateTime(go).toString()));
            // }
            return this;
        }

        public Condition between(String propertyName, Number lo, Number go) {
            if (!(isNullOrEmpty(lo)))
                ge(propertyName, lo);

            if (!(isNullOrEmpty(go)))
                le(propertyName, go);
            return this;
        }

        /**
         * 小于等于
         *
         * @param propertyName 属性名称
         * @param value
         */
        public Condition le(String propertyName, Number value) {

            predicates.add(criteriaBuilder.le(from.get(propertyName), value));
            return this;
        }

        /**
         * 小于
         *
         * @param propertyName 属性名称
         * @param value
         */
        public Condition lt(String propertyName, Number value) {

            predicates.add(criteriaBuilder.lt(from.get(propertyName), value));
            return this;
        }

        /**
         * 大于等于
         *
         * @param propertyName 属性名称
         * @param value
         */
        public Condition ge(String propertyName, Number value) {

            predicates.add(criteriaBuilder.ge(from.get(propertyName), getParameterExpression(value)));
            return this;
        }

        /**
         * 大于
         *
         * @param propertyName 属性名称
         * @param value
         */
        public Condition gt(String propertyName, Number value) {

            predicates.add(criteriaBuilder.gt(from.get(propertyName), getParameterExpression(value)));
            return this;
        }

        /**
         * 直接添加JPA内部的查询条件,用于应付一些复杂查询的情况,例如或
         */
        public void addCriteria(Predicate predicate) {
            predicates.add(predicate);
        }

        private ParameterExpression getParameterExpression(Object value) {
            ParameterExpression parameter = criteriaBuilder.parameter(value.getClass());
            parameterInfos.add(new ParameterInfo(parameter, value));
            return parameter;
        }

        public T get() {
            return CriteriaQueryJPA.this.get();
        }

        public List<T> list() {
            return CriteriaQueryJPA.this.list();
        }

        public PagingResult<T> listPaging(int page, int maxResult) {
            return CriteriaQueryJPA.this.listPaging(page, maxResult);
        }

        public long count () {
            return CriteriaQueryJPA.this.count();
        }
        public Object sum(String propertyName) {
            return CriteriaQueryJPA.this.sum(propertyName);
        }
    }


    private boolean isNullOrEmpty(Object value) {
        if (value instanceof String) {
            return value == null || "".equals(value);
        }
        return value == null;
    }


    public T get() {
        TypedQuery<T> query = entityManager.createQuery(this.populate());
        for (ParameterInfo parameterInfo : parameterInfos) {
            query.setParameter(parameterInfo.getParameterExpression(), parameterInfo.getValue());
        }
        try {
            return query.getSingleResult();
        } catch (NoResultException e) {
            return null;
        }
    }


    public List<T> list() {
        TypedQuery<T> query = entityManager.createQuery(this.populate());
        for (ParameterInfo parameterInfo : parameterInfos) {
            query.setParameter(parameterInfo.getParameterExpression(), parameterInfo.getValue());
        }
        return query.getResultList();
    }

    public PagingResult<T> listPaging(int page, int maxResult) {
        CriteriaQuery criteriaQuery = this.populate();
        TypedQuery<T> query = entityManager.createQuery(criteriaQuery);

        setPageInfo(page, maxResult, query);

        for (ParameterInfo parameterInfo : parameterInfos) {
            query.setParameter(parameterInfo.getParameterExpression(), parameterInfo.getValue());
        }

        return new PagingResult<T>(query.getResultList(), count(criteriaQuery), page, maxResult);
    }

    private Object sum(String propertyName, CriteriaQuery criteriaQuery) {
        criteriaQuery.select(criteriaBuilder.sum(from.get(propertyName)));

        TypedQuery countQuery = entityManager.createQuery(criteriaQuery);
        for (ParameterInfo parameterInfo : parameterInfos) {
            countQuery.setParameter(parameterInfo.getParameterExpression(), parameterInfo.getValue());
        }

        try {
            return countQuery.getSingleResult();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private long count(CriteriaQuery criteriaQuery) {
        criteriaQuery.select(this.criteriaBuilder.tuple(criteriaBuilder.count(from)));

        TypedQuery countQuery = entityManager.createQuery(criteriaQuery);
        for (ParameterInfo parameterInfo : parameterInfos) {
            countQuery.setParameter(parameterInfo.getParameterExpression(), parameterInfo.getValue());
        }

        return (long) countQuery.getSingleResult();
    }

    public long count() {
        return this.count(this.populate());
    }

    public Object sum(String propertyName) {
        return this.sum(propertyName, this.populate());
    }

    private void setPageInfo(int page, int maxResult, TypedQuery<T> query) {
        maxResult = checkResultPerPage(maxResult);

        if (page <= 0) {
            page = 1;
        }

        if (page >= 0 && maxResult >= 0) {
            query
                    .setFirstResult((page - 1) * maxResult)
                    .setMaxResults(maxResult);

        }
    }

    private static int MAX_RESULT_PER_PAGE = 100;

    private int checkResultPerPage(int maxResult) {
        if (maxResult > MAX_RESULT_PER_PAGE) {
            maxResult = MAX_RESULT_PER_PAGE;
        }
        return maxResult;
    }

    public CriteriaQueryJPA<T> groupBy(String groupBy) {
        this.groupBy = groupBy;
        return this;
    }

    public CriteriaQueryJPA<T> addOrderBy(String propertyName, OrderBy order) {

        if (this.orders == null)
            this.orders = new ArrayList();

        if (order == OrderBy.ASC)
            this.orders.add(criteriaBuilder.asc(from.get(propertyName)));
        else if (order == OrderBy.DESC)
            this.orders.add(criteriaBuilder.desc(from.get(propertyName)));
        return this;
    }

    public CriteriaQueryJPA<T> orderBy(String propertyName, OrderBy order) {
        this.orders = null;
        addOrderBy(propertyName, order);
        return this;
    }

    /**
     * 创建查询条件
     *
     * @return
     */
    CriteriaQuery populate() {
        criteriaQuery.where(predicates.toArray(new Predicate[predicates.size()]));
        if (!isNullOrEmpty(groupBy)) {
            criteriaQuery.groupBy(from.get(groupBy));
        }
        if (this.orders != null) {
            criteriaQuery.orderBy(orders);
        }
        //addLinkCondition(this);
        return criteriaQuery;
    }

    private void addLinkCondition(CriteriaQueryJPA query) {

        Map subQuery = query.linkQuery;
        if (subQuery == null)
            return;

        for (Iterator queryIterator = subQuery.keySet().iterator(); queryIterator.hasNext(); ) {
            String key = (String) queryIterator.next();
            CriteriaQueryJPA sub = (CriteriaQueryJPA) subQuery.get(key);
            from.join(key);
//            criteriaQuery.where(sub.predicates.<Predicate>toArray(new Predicate[0]));
//            addLinkCondition(sub);
        }
    }


    public Class getModelClass() {
        return this.clazz;
    }

    public String getProjection() {
        return this.projection;
    }

    public void setProjection(String projection) {
        this.projection = projection;
    }

    public Class getClazz() {
        return this.clazz;
    }

    public List<Order> getOrders() {
        return orders;
    }

    public void setOrders(List<Order> orders) {
        this.orders = orders;
    }

    public EntityManager getEntityManager() {
        return this.entityManager;
    }

    public void setEntityManager(EntityManager em) {
        this.entityManager = em;
    }

    public Root getFrom() {
        return from;
    }

    public List<Predicate> getPredicates() {
        return predicates;
    }

    public void setPredicates(List<Predicate> predicates) {
        predicates = predicates;
    }

    public javax.persistence.criteria.CriteriaQuery getCriteriaQuery() {
        return criteriaQuery;
    }

    public CriteriaBuilder getCriteriaBuilder() {
        return criteriaBuilder;
    }

    public void setFetchModes(List<String> fetchField, List<String> fetchMode) {

    }

    public String getGroupBy() {
        return groupBy;
    }


}